<?php

namespace Upfor\UpforPHP;

use Closure;
use Exception;
use Upfor\UpforPHP\Http\Route;
use Upfor\UpforPHP\Http\Router;
use Upfor\UpforPHP\Http\Request;
use Upfor\UpforPHP\Http\Response;
use Whoops\Run as WhoopsHandle;
use Whoops\Handler\PlainTextHandler;
use Whoops\Handler\PrettyPageHandler;
use Whoops\Handler\CallbackHandler;
use Upfor\UpforPHP\Library\ErrorLogger;

/**
 * App engine
 */
class App {

    /**
     * @var Router
     */
    public $router;

    /**
     * @var Request
     */
    public $request;

    /**
     * @var Response
     */
    public $response;

    /**
     * @var array
     */
    protected $hooks = array(
        'upfor.before' => null,
        'upfor.before.dispatch' => null,
        'upfor.after.dispatch' => null,
        'upfor.after' => null,
    );

    public function __construct() {
        $this->request = request();
        $this->response = response();
        $this->router = router();
    }

    /**
     * Assign hook
     *
     * @param  string $name     The hook name
     * @param  mixed  $callable A callable object
     */
    public function hook($name, $callable) {
        if (is_callable($callable)) {
            $this->hooks[$name] = $callable;
        }
    }

    /**
     * Invoke hook
     *
     * @param  string $name    The hook name
     * @param  mixed  $hookArg (optional) Argument for hooked functions
     * @return mixed
     */
    public function applyHook($name, $hookArg = null) {
        if (!$this->hooks[$name] || !is_callable($this->hooks[$name])) {
            return false;
        }

        return call_user_func($this->hooks[$name], $hookArg);
    }

    /**
     * Redirect
     *
     * @param  string $url    The destination URL
     * @param  int    $status The HTTP redirect status code (optional)
     */
    public function redirect($url, $status = 302) {
        $this->response->clear();
        $this->response->status($status);
        $this->response->header('Location', $url);
    }

    /**
     * Run
     */
    public function run() {
        try {
            $this->errorHandle();

            $this->applyHook('upfor.before');
            ob_start();
            $dispatched = false;
            $matchedRoutes = $this->router->getMatchedRoutes($this->request->getMethod(), $this->request->getPathInfo());

            foreach ($matchedRoutes as $route) {
                $this->router->setCurrentRoute($route);
                $this->applyHook('upfor.before.dispatch');
                $dispatched = $route->dispatch($this);
                $this->applyHook('upfor.after.dispatch');
                if ($dispatched !== false) {
                    break;
                }
            }

            if ($dispatched === false) {
                echo 'Page Not Found';
                $this->response->status(404);
            }

            $this->response->body(ob_get_clean());
            $this->response->send();
            $this->applyHook('upfor.after');
        } catch (Exception $e) {
            throw $e;
        }
    }

    /**
     * Clean current output buffer
     */
    protected function cleanBuffer() {
        if (ob_get_level() !== 0) {
            ob_clean();
        }
    }

    /**
     * Error handle
     */
    protected function errorHandle() {
        // 注册whoops进行错误处理
        ini_set('display_errors', 0);
        error_reporting(E_ALL ^ E_NOTICE);
        $whoops = new WhoopsHandle();
        if (config('debug')) {
            ini_set('display_startup_errors', true);

            $prettyPageHandler = new PrettyPageHandler();
            $whoops->pushHandler($prettyPageHandler);
        } else {
            // Log error
            $plainTextHandler = new PlainTextHandler(ErrorLogger::getInstance());
            $plainTextHandler->loggerOnly(true);
            $whoops->pushHandler($plainTextHandler);

            // Display human page for exception
            $errorCallback = function ($exception, $inspector, $run) {
                $exceptionName = explode('\\', get_class($exception));
                $exceptionName = array_pop($exceptionName);
                $extension = isset(config('view')['config']['extension']) ? config('view')['config']['extension'] : '';
                $exceptionPageName = file_exists(config('errorPageDirectory') . '/' . $exceptionName . $extension) ? $exceptionName : 'Exception';
                $exceptionPagePath = config('errorPageDirectory') . '/' . $exceptionPageName;
                view()->display($exceptionPagePath, ['exceptionName' => $exceptionName]);
            };

            $CallbackHandler = new CallbackHandler($errorCallback);
            $whoops->pushHandler($CallbackHandler);
        }
        $whoops->register();
    }

    /**
     * Add route with multiple methods
     *
     * @param  array           $methods  Numeric array of HTTP method names
     * @param  string          $pattern  The route URI pattern
     * @param  callable|string $callback The route callback routine
     * @return Route
     */
    public function map(array $methods, $pattern, $callback) {
        if ($callback instanceof Closure) {
            $callback = $callback->bindTo($this);
        }

        return $this->router->map($methods, $pattern, $callback, config('route')['caseSensitive']);
    }

    /**
     * Add GET route
     *
     * @param  string          $pattern  The route URI pattern
     * @param  callable|string $callback The route callback routine
     *
     * @return Route
     */
    public function get($pattern, $callback) {
        return $this->map(['GET'], $pattern, $callback);
    }

    /**
     * Add POST route
     *
     * @param  string          $pattern  The route URI pattern
     * @param  callable|string $callback The route callback routine
     *
     * @return Route
     */
    public function post($pattern, $callback) {
        return $this->map(['POST'], $pattern, $callback);
    }

    /**
     * Add PUT route
     *
     * @param  string          $pattern  The route URI pattern
     * @param  callable|string $callback The route callback routine
     *
     * @return Route
     */
    public function put($pattern, $callback) {
        return $this->map(['PUT'], $pattern, $callback);
    }

    /**
     * Add PATCH route
     *
     * @param  string          $pattern  The route URI pattern
     * @param  callable|string $callback The route callback routine
     *
     * @return Route
     */
    public function patch($pattern, $callback) {
        return $this->map(['PATCH'], $pattern, $callback);
    }

    /**
     * Add DELETE route
     *
     * @param  string          $pattern  The route URI pattern
     * @param  callable|string $callback The route callback routine
     *
     * @return Route
     */
    public function delete($pattern, $callback) {
        return $this->map(['DELETE'], $pattern, $callback);
    }

    /**
     * Add OPTIONS route
     *
     * @param  string          $pattern  The route URI pattern
     * @param  callable|string $callback The route callback routine
     *
     * @return Route
     */
    public function options($pattern, $callback) {
        return $this->map(['OPTIONS'], $pattern, $callback);
    }

    /**
     * Add route for any HTTP method
     *
     * @param  string          $pattern  The route URI pattern
     * @param  callable|string $callback The route callback routine
     *
     * @return Route
     */
    public function any($pattern, $callback) {
        return $this->map($this->router->getSupportMethods(), $pattern, $callback);
    }

}
